#include <jni.h>
#include <string>
#include <android/log.h>
#include <android/native_window_jni.h>
#include <EGL/egl.h>
#include <GLES2/gl2.h>
#include <string.h>

#define LOGD(...) __android_log_print(ANDROID_LOG_WARN,"Test OpenGL ES ",__VA_ARGS__)

//着色器之间不能直接通信，每个着色器都是独立的小程序，它们唯一的交流信息就是输入和输出参数
//着色器的小程序采用GLSL语言(OpenGL Shader Language)编写

//常用的限定符主要有attribute、varying、in、out、uniform五个，分别说明如下。
// attribute：表示该变量是输入参数。GLSL 2.0使用。
// varying：表示该变量是输出参数。GLSL 2.0使用。
// in：表示该变量是输入参数。GLSL 3.0使用。
// out：表示该变量是输出参数。GLSL 3.0使用。
// uniform：表示该变量是全局参数。

// **顶点着色器** glsl                       确定位置
//以下场景，顶点着色器 的模板几乎是固定的
//✅ 在屏幕上渲染一张2D纹理（如图片/视频）。
//✅ 处理坐标系差异（如YUV数据）。
//✅ 简单的全屏绘制。
#define GET_STR(x) #x
static const char *vertexShader = GET_STR(
        attribute vec4 aPosition; //顶点坐标
        attribute vec2 aTexCoord; //材质顶点坐标
        varying vec2 vTexCoord;   //输出的材质坐标(处理后的纹理坐标（传递给片元着色器）)
        //声明完数据变量之后，即可编写形如void main() { /*里面是具体的实现代码*/ }的小程序代码。
        void main() {
            // 翻转Y坐标        翻转的原因：确保图像方向正确。
            vTexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y);
            //简单投影：代码中顶点坐标已经是裁剪空间坐标（范围[-1,1]），直接赋值即可全屏渲染。例如：顶点坐标(1.0, -1.0, 0.0)对应屏幕右下角。
            gl_Position = aPosition;//给内置的位置变量赋值
        }
);

//片元着色器,软解码和部分x86硬解码
//这段片元着色器代码是专门为YUV420P格式视频数据转RGB渲染设计的（细节部分暂时没有必要关注）
static const char *fragYUV420P = GET_STR(
        precision mediump float;    //中等精度浮点数 作用：平衡性能和精度，适合移动端GPU（OpenGL ES要求必须声明精度）。
        varying vec2 vTexCoord;     //顶点着色器传递的坐标
        //YUV420P数据存储为三个独立平面（Y全分辨率，U/V半分辨率），需分别采样。
        uniform sampler2D yTexture; //输入的材质（不透明灰度，单像素）
        uniform sampler2D uTexture;
        uniform sampler2D vTexture;
        void main() {
            vec3 yuv;
            vec3 rgb;
            yuv.r = texture2D(yTexture, vTexCoord).r;
            yuv.g = texture2D(uTexture, vTexCoord).r - 0.5;
            yuv.b = texture2D(vTexture, vTexCoord).r - 0.5;
            rgb = mat3(1.0, 1.0, 1.0,
                       0.0, -0.39465, 2.03211,
                       1.13983, -0.58060, 0.0) * yuv;
            //输出像素颜色
            gl_FragColor = vec4(rgb, 1.0);
        }
);

GLint InitShader(const char *code, GLint type) {
    //8.创建指定类型的着色器并返回着色器的编号。输入参数填着色器的类型，其中 GL_VERTEX_SHADER 表示顶点着色器，GL_FRAGMENT_SHADER 表示片段（元）着色器。
    GLint sh = glCreateShader(type);
    if (sh == 0) {
        LOGD("glCreateShader %d failed!", type);
        return 0;
    }

    //9.指定着色器的程序内容。 第一个参数填着色器编号，第二个参数填1，表示1个着色器， 第三个参数填着色器的代码字符串
    glShaderSource(sh,
                   1,    //shader数量
                   &code, //shader代码
                   0);   //代码长度

    //10.编译着色器的程序代码。输入参数填着色器编号
    glCompileShader(sh);

    //11.获取编译情况
    GLint status;
    glGetShaderiv(sh, GL_COMPILE_STATUS, &status);
    if (status == 0) {
        LOGD("glCompileShader failed!");
        return 0;
    }
    LOGD("glCompileShader success!");
    return sh;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_jack_ffmpeg_1simple02_PlayView_PlayYuv(JNIEnv *env, jobject thiz, jstring url_,
                                                jobject surface) {
    const char *url = env->GetStringUTFChars(url_, 0);
    LOGD("open url is %s", url);

    FILE *fp = fopen(url, "rb");
    if (!fp) {
        LOGD("open file %s failed!", url);
        return;
    }

    //1-7：可以看成 ***** 步骤一，初始化EGL & 让EGL接管原生窗口ANativeWindow *****
    //尽管EGL本身属于接口层，但EGL的表面对象不是凭空产生的，而是从原生窗口接管而来的。
    //要引入ANativeWindow，从原生窗口接管表面对象，然后才能创建用于OpenGL ES的EGL环境
    //1.获取原始窗口---从Surface获取原生窗口
    ANativeWindow *nwin = ANativeWindow_fromSurface(env, surface);

    //2.获取EGL显示器
    EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    if (display == EGL_NO_DISPLAY) {
        LOGD("eglGetDisplay failed!");
        return;
    }
    if (EGL_TRUE != eglInitialize(display, 0, 0)) {
        LOGD("eglInitialize failed!");
        return;
    }
    //3.输出配置
    // EGL配置
    EGLConfig config;
    // 配置数量
    EGLint configNum;
    //配置规格，涉及RGB颜色空间
    EGLint configSpec[] = {
            EGL_RED_SIZE, 8,
            EGL_GREEN_SIZE, 8,
            EGL_BLUE_SIZE, 8,
            EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_NONE
    };
    //4.eglChooseConfig:选择配置。给EGL显示器选择最佳配置。第一个参数为EGLDisplay类型的显示器变量，第二个参数为指定了RGB颜色空间的配置规格，第三个参数为EGLConfig类型的配置变量。
    if (EGL_TRUE != eglChooseConfig(display, configSpec, &config, 1, &configNum)) {
        LOGD("eglChooseConfig failed!");
        return;
    }
    //5.创建EGL表面。这里EGL接管了原生窗口的表面对象
    EGLSurface winsurface = eglCreateWindowSurface(display, config, nwin, 0);
    if (winsurface == EGL_NO_SURFACE) {
        LOGD("eglCreateWindowSurface failed!");
        return;
    }

    const EGLint ctxAttr[] = {
            EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE
    };
    //6.结合 EGL显示器 与 EGL配置创建EGL 实例
    EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, ctxAttr);
    if (context == EGL_NO_CONTEXT) {
        LOGD("eglCreateContext failed!");
        return;
    }
    //7.创建EGL环境，之后即可执行OpenGL的相关操作。第一个参数为EGLDisplay类型的显示器变量，第二个参数为绘制需要的EGL表面变量，第三个参数为读取需要的EGL表面变量
    if (EGL_TRUE != eglMakeCurrent(display, winsurface, winsurface, context)) {
        LOGD("eglMakeCurrent failed!");
        return;
    }

    LOGD("EGL Init Success!");

    //8-24 可以看成 ***** 步骤二，初始化OpenGL ES *****
    //该部分是跟着色器纹理有关的，这三个部分可以划分为3个小一点的步骤
    //8-16，划分到步骤01中. ***** 分别依据对应小程序，初始化顶点着色器和片段着色器，并获取着色器链接后的小程序编号. *****
    //17-19，划分到步骤02中，***** 根据小程序编号设置 顶点坐标 和 材质坐标 *****
    //20-24，划分到步骤03中，***** 分别创建Y、U、V三个分量的纹理，并分别设置三个纹理分量的规格与材质	 *****
    //8-11，顶点和片元shader初始化
    //顶点shader初始化
    GLint vsh = InitShader(vertexShader, GL_VERTEX_SHADER);
    //片元yuv420 shader初始化
    GLint fsh = InitShader(fragYUV420P, GL_FRAGMENT_SHADER);

    //12.创建小程序，并返回小程序的编号
    GLint program = glCreateProgram();
    if (program == 0) {
        LOGD("glCreateProgram failed!");
        return;
    }
    //13.将着色器的编译结果添加至小程序。第一个参数填小程序编号，第二个参数填着色器编号
    glAttachShader(program, vsh);
    glAttachShader(program, fsh);

    //14.链接着色器的小程序。输入参数填小程序编号
    glLinkProgram(program);
    GLint status = 0;
    //15.检查着色器链接是否成功。 第一个参数填小程序编号， 第二个参数填 GL_LINK_STATUS ，第三个参数填待返回的状态变量。 状态值为GL_TRUE表示成功，其他表示失败。
    glGetProgramiv(program, GL_LINK_STATUS, &status);
    if (status != GL_TRUE) {
        LOGD("glLinkProgram failed!");
        return;
    }
    //16.使用小程序。输入参数填小程序编号
    glUseProgram(program);
    LOGD("glLinkProgram success!");


    //加入三维顶点数据 两个三角形组成正方形
    static float vers[] = {
            1.0f, -1.0f, 0.0f,
            -1.0f, -1.0f, 0.0f,
            1.0f, 1.0f, 0.0f,
            -1.0f, 1.0f, 0.0f,
    };

    //17.从小程序获取属性变量的位置索引。第一个参数填小程序编号，第二个参数填属性变量的名称。输出参数为属性变量的位置索引
    GLuint apos = (GLuint) glGetAttribLocation(program, "aPosition");
    //18.启用顶点属性数组。输入参数为属性变量的位置索引
    glEnableVertexAttribArray(apos);
    //19.指定顶点属性数组的位置索引及其数据格式。第一个参数填属性变量的位置索引；第二个参数填属性的长度，对于三维空间填3，因为三维空间有x、y、z三个方向，对于二维空间填2，因为二维空间只有x、y两个方向
    glVertexAttribPointer(apos, 3, GL_FLOAT, GL_FALSE, 12, vers);

    // 上面的三个函数要分别调用两轮，其中第一轮的 glGetAttribLocation → glEnableVertexAttribArray → glVertexAttribPointer 设置顶点坐标，
    // 第二轮的glGetAttribLocation→glEnableVertexAttribArray→glVertexAttribPointer设置材质坐标。
    // 之所以设置完 顶点坐标 还要设置 材质坐标 ，是因为后面要往顶点组成的轮廓粘贴图像纹理，才能呈现最终的空间景象。这里的图像纹理就来自视频帧的YUV图像。
    //故：下方的就不做过多注释

    //加入材质坐标数据
    static float txts[] = {
            1.0f, 0.0f, //右下
            0.0f, 0.0f,
            1.0f, 1.0f,
            0.0, 1.0
    };
    GLuint atex = (GLuint) glGetAttribLocation(program, "aTexCoord");
    glEnableVertexAttribArray(atex);
    glVertexAttribPointer(atex, 2, GL_FLOAT, GL_FALSE, 8, txts);


    //注意：测试，写定的数据（要跟YUV中的宽高保持一致，否则画面显示会存在异常）
    int width = 424;
    int height = 240;

    //20.设置纹理层
    //glGetUniformLocation：获取纹理在小程序中的位置。第一个参数填小程序编号，第二个参数填纹理的名称。输出参数为纹理在小程序中的位置
    //glUniform1i：设置纹理层。第一个参数为纹理在小程序中的位置，第二个参数为纹理序号
    glUniform1i(glGetUniformLocation(program, "yTexture"), 0); //对于纹理第1层
    glUniform1i(glGetUniformLocation(program, "uTexture"), 1); //对于纹理第2层
    glUniform1i(glGetUniformLocation(program, "vTexture"), 2); //对于纹理第3层

    //21.创建opengl纹理
    GLuint texts[3] = {0};
    //glGenTextures：创建纹理数组。第一个参数为数组长度，填3表示有三个色彩分量；第二个参数填待创建的纹理数组。
    glGenTextures(3, texts);

    //22.绑定指定纹理（设置纹理属性）。第一个参数填GL_TEXTURE_2D，第二个参数填具体纹理，比如下标为0的纹理数组元素表示采用第一个分量的纹理。
    glBindTexture(GL_TEXTURE_2D, texts[0]);

    //23.设置纹理的过滤器
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    //24.设置纹理的规格与材质。glTexImage2D：对于Y分量来说，其宽高就是视频的宽高；对U分量和V分量来说，其宽高各为视频宽高的一半。最后一个参数填当前分量的缓存数据。

    // ***** 注意事项 *****
    //调用 glTexImage2D 函数时，最后一个参数非空表示直接渲染纹理。
    // 对于YUV空间来说，每个视频帧对三个分量各自调用 glUniform1i → glActiveTexture → glBindTexture → glTexParameteri → glTexImage2D ，
    // 最后只要调用一次 glDrawArrays 函数即可完成该帧图像的绘制操作。

    //调用 glTexImage2D 函数时，最后一个参数为空表示要分两步渲染纹理。
    // 第一步，对三个分量各自调用 glUniform1i→glBindTexture → glTexParameteri → glTexImage2D，表示先占个位；
    // 第二步，每个视频帧对三个分量各自调用 glActiveTexture → glBindTexture → glTexSubImage2D，表示替换当前分量的缓存数据，
    // 最后只要调用一次 glDrawArrays 函数即可完成该帧图像的绘制操作。
    // ***** 注意事项 *****

    glTexImage2D(GL_TEXTURE_2D,
                 0,                       //细节基本 0默认
                 GL_LUMINANCE,     //gpu内部格式 亮度，灰度图
                 width, height,                 //拉升到全屏
                 0,                     //边框
                 GL_LUMINANCE,          //数据的像素格式 亮度，灰度图 要与上面一致
                 GL_UNSIGNED_BYTE,        //像素的数据类型
                 NULL                    //纹理的数据
    );
    glBindTexture(GL_TEXTURE_2D, texts[1]);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    glTexImage2D(GL_TEXTURE_2D,
                 0,
                 GL_LUMINANCE,
                 width / 2, height / 2,
                 0,
                 GL_LUMINANCE,
                 GL_UNSIGNED_BYTE,
                 NULL
    );
    glBindTexture(GL_TEXTURE_2D, texts[2]);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    glTexImage2D(GL_TEXTURE_2D,
                 0,
                 GL_LUMINANCE,
                 width / 2, height / 2,
                 0,
                 GL_LUMINANCE,
                 GL_UNSIGNED_BYTE,
                 NULL
    );

    // 25-29 可以看成 ***** 步骤三 通过OpenGL ES渲染视频画面，轮询每个视频帧的时候，需要把视频帧的YUV数据写入对应的ES纹理 （for循环已经做了该部分） *****
    //轮询每个视频帧的时候，需要把视频帧的YUV数据写入对应的ES纹理
    //将file读取的内容,Y U V分别使用三个数组来临时存储    实际项目不要这么来写，需要通过一定的封装
    unsigned char *buf[3] = {0};
    buf[0] = new unsigned char[width * height];
    buf[1] = new unsigned char[width * height / 4];
    buf[2] = new unsigned char[width * height / 4];

    //模拟操作，实际项目不要这么写
    for (int i = 0; i < 10000; i++) {
        if (feof(fp) == 0) {
            fread(buf[0], 1, width * height, fp);
            fread(buf[1], 1, width * height / 4, fp);
            fread(buf[2], 1, width * height / 4, fp);
        }

        //激活第1层纹理,绑定到创建的opengl纹理
        //25.激活纹理
        glActiveTexture(GL_TEXTURE0);
        //26.绑定指定纹理
        glBindTexture(GL_TEXTURE_2D, texts[0]);
        //27.替换纹理内容。最后一个参数填当前分量的缓存数据
        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_LUMINANCE, GL_UNSIGNED_BYTE,
                        buf[0]);

        //第2层纹理 同理
        glActiveTexture(GL_TEXTURE0 + 1);
        glBindTexture(GL_TEXTURE_2D, texts[1]);
        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width / 2, height / 2, GL_LUMINANCE,
                        GL_UNSIGNED_BYTE, buf[1]);

        //第3层纹理同理
        glActiveTexture(GL_TEXTURE0 + 2);
        glBindTexture(GL_TEXTURE_2D, texts[2]);
        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width / 2, height / 2, GL_LUMINANCE,
                        GL_UNSIGNED_BYTE, buf[2]);

        //28.采用顶点的坐标数组方式绘制图形
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

        //29.将OpenGL的纹理缓存显示到屏幕上。第一个参数为EGLDisplay类型的显示器变量，第二个参数为EGLSurface类型的EGL表面变量。
        //把OpenGL ES的纹理缓存显示到屏幕上：在调用OpenGL ES的绘制函数之后，还要调用EGL的eglSwapBuffers函数，才能把OpenGL ES的纹理缓存显示到屏幕上
        eglSwapBuffers(display, winsurface);
    }

    // ***** 步骤四：释放EGL资源 *****
    //视频遍历结束，除释放FFmpeg相关的实例资源外，还要释放EGL的表面和实例资源，包括EGL用到的原生窗口也要释放。
    // 释放原生窗口
    // 销毁EGL表面
    // 销毁EGL实例
    env->ReleaseStringUTFChars(url_, url);
}